﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.DirectoryServices.Protocols;
using System.Reflection;
using System.Text;
using ActiveDirectoryUtilities.DirectoryServiceAccess;
using System.IO;

namespace ActiveDirectoryUtilities.SearchForDuplicateAttributeData
{
	class Program
	{
		// Fields
		private static string _attribute = "servicePrincipalName";
		private static LDAP_PORTS _bindTo = LDAP_PORTS.LDAP;
		private static Dictionary<string, Dictionary<string, DateTime>> _attributeData = new Dictionary<string, Dictionary<string, DateTime>>();
		private static string _targetDC = string.Empty;
		private static string _targetNC = string.Empty;
		private static string _ldapFilter = string.Empty;
		private static byte _trackActionIndicatorToShow = 0;
		private static bool _diagnostics = false;
		private const string CMD_LINE_SWITCH_ATTRIBUTE_NAME = "AttributeName";
		private const string CMD_LINE_SWITCH_TARGET_DC = "TargetDC";
		private const string CMD_LINE_SWITCH_TARGET_NC = "TargetNC";
		private const string CMD_LINE_SWITCH_USE_GC = "UseGC";
		private const string CMD_LINE_SWITCH_LDAP_FILTER = "LdapFilter";
		private const string CMD_LINE_SWITCH_DIAG = "ShowDiagnostics";

		private static void Main(string[] args)
		{
			AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

			outputHeader();

			if ((Environment.Version.Major < 2) || ((Environment.Version.Major == 2) && (Environment.Version.Revision < 1433)))
			{
				Console.WriteLine(".NET 2.0 SP1 does not appear to be installed (detected " +
					Environment.Version.ToString() + ") .  Please goto http://www.microsoft.com/downloads and install .NET 2.0 SP1");
				return;
			}

			if (!parseCommandLineArgs(args))
			{
				return;
			}

			if (string.Compare(_attribute, "servicePrincipalName", StringComparison.OrdinalIgnoreCase) == 0)
			{
				Console.WriteLine("Reference http://support.microsoft.com/kb/321044/en-us for details.");
			}

			//If the root NC is empty, assume that the entire forest should be searched.
			//  If the attribute exists on a GC, the query must target a specific server and must occur on a global catalog port otherwise an exception regarding no objects being found will be returned.
			//  If the attribute is not in the PAS, iterate through each domain in the forest.
			if (string.IsNullOrEmpty(_targetNC))
			{
				//TODO:  Check if the attribute is in the PAS
				bool isInPAS = true;
				if (isInPAS)
				{
					RootDse root = null;
					try
					{
						root = new RootDse(string.Empty, (int)LDAP_PORTS.Global_Catalog);
					}
					catch (LdapException ex)
					{
						Console.WriteLine(ex.Message);
						return;
					}
					catch (DirectoryException ex)
					{
						Console.WriteLine(ex.Message);
						return;
					}

					_bindTo = LDAP_PORTS.Global_Catalog;
					if (!collectData(root.DnsHostName, string.Empty))
					{
						return;
					}
				}
				else
				{
					_bindTo = LDAP_PORTS.LDAP;
					Forest f = Forest.GetCurrentForest();
					foreach (Domain d in f.Domains)
					{
						if (!collectData(string.Empty, d.NcName))
						{
							return;
						}
					}
				}
			}
			else
			{
				if (!collectData(_targetDC, _targetNC))
				{
					return;
				}
			}

			bool areDuplicatesFound = false;
			string fileName = "duplicate_" + _attribute + ".txt";
			if (_attributeData.Count >= 0)
			{
				using (FileStream fsOut = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Read))
				{
					using (StreamWriter swOut = new StreamWriter(fsOut, Encoding.Unicode))
					{
						foreach (KeyValuePair<string, Dictionary<string, DateTime>> pair in _attributeData)
						{
							if ((pair.Value.Count > 1) && (pair.Key.ToLower() != "kadmin/changepw"))
							{
								areDuplicatesFound = true;
								swOut.WriteLine(pair.Key + ":  " + pair.Value.Count.ToString());
								foreach (KeyValuePair<string, DateTime> kvp in pair.Value)
								{
									swOut.WriteLine("\t" + kvp.Key + " - " + kvp.Value);
								}
								continue;
							}
						}
					}
				}
			}

			if (!areDuplicatesFound)
			{
				if (File.Exists(fileName))
				{
					File.Delete(fileName);
				}
				Console.WriteLine("No duplicates found.");
			}
			else
			{
				Console.WriteLine("Duplicates saved to {0}.", fileName);
			}
		}

		private static bool collectData(string targetDC, string rootNC)
		{
			string[] attributesToReturn = new string[] { _attribute };

			//Handle special case where the duplicate being looked for is servicePrincipalName attribute
			//	Active directory will automatically add in host/netBiosName, host/dnsHostName.contoso.com, cifs/netBiosName, cifs/dnsHostName.contoso.com if not explicitly defined
			//  ensure that if the object is a computer object, we check for these "virtual" SPNs when not explicitly defined.
			if (string.Compare(_attribute, "servicePrincipalName", StringComparison.OrdinalIgnoreCase) == 0)
			{
				attributesToReturn = new string[] { _attribute, "objectClass", "dnsHostName", "samAccountName", "whenChanged" };
			}

			if (!string.IsNullOrEmpty(_ldapFilter))
			{
				_ldapFilter = string.Format("(&{0}({1}=*))", _ldapFilter, _attribute);
			}
			else
			{
				_ldapFilter = string.Format("({0}=*)", _attribute);
			}

			using (WLdapSearchRequest wlsr = new WLdapSearchRequest(
				_targetDC
				, (int)_bindTo
				, rootNC
				, _ldapFilter
				, SearchScope.Subtree
				, attributesToReturn))
			{
				WLdapSearchResultCollection wsrc = null;
				try
				{
					wsrc = wlsr.FindAll();
				}
				catch (LdapException ex)
				{
					Console.WriteLine(ex.Message);
					return false;
				}
				catch (DirectoryException ex)
				{
					Console.WriteLine(ex.Message);
					return false;
				}

				if (wsrc != null)
				{
					foreach (SearchResultEntry sre in wsrc)
					{
						string objectDN = sre.DistinguishedName.ToLowerInvariant();
						System.Diagnostics.Debug.WriteLine("distinguishedName - " + objectDN);

						#region servicePrincipalName specific checks
						string objectClass = string.Empty;

						DateTime whenChanged = GlobalMethods.ConvertGeneralizedTimeToDateTime(sre.Attributes["whenChanged"]);

						if (string.Compare(_attribute, "servicePrincipalName", StringComparison.OrdinalIgnoreCase) == 0)
						{
							if (sre.Attributes.Contains("objectClass"))
							{
								objectClass = sre.Attributes["objectClass"][sre.Attributes["objectClass"].Count - 1].ToString();
								#region code to check for implicit SPNs.  Only happens on computer schema class objects
								if (string.Compare(objectClass, "computer", StringComparison.OrdinalIgnoreCase) == 0)
								{
									if (sre.Attributes.Contains("dnsHostName"))
									{
										string dnsHostName = sre.Attributes["dnsHostName"][0].ToString().ToLowerInvariant();
										updateDataList(objectDN, whenChanged, "host/" + dnsHostName);
										updateDataList(objectDN, whenChanged, "cifs/" + dnsHostName);
									}
									else
									{
										Console.WriteLine("Error:  Could not read dnsHostName for {0}.  This may prevent proper identification of all servicePrincipalNames in use for this object.", sre.DistinguishedName);
									}

									if (sre.Attributes.Contains("samAccountName"))
									{
										string netBiosName = sre.Attributes["samAccountName"][0].ToString().TrimEnd(new char[] { '$' }).ToLowerInvariant();
										updateDataList(objectDN, whenChanged, "host/" + netBiosName);
										updateDataList(objectDN, whenChanged, "cifs/" + netBiosName);
									}
									else
									{
										Console.WriteLine("Error:  Could not read samAccountName for {0}.  This may prevent proper identification of all servicePrincipalNames in use for this object.");
									}
								}
								#endregion
							}
							else
							{
								Console.WriteLine("Error:  Could not determine objectClass for {0}.  To properly evaluate servicePrincipalName the account in use must have read access to this ID.", sre.DistinguishedName);
							}
							System.Diagnostics.Debug.WriteLine("objectClass - " + objectClass);
						}
						#endregion

						if (sre.Attributes.Contains(_attribute))
						{
							foreach (byte[] ba in sre.Attributes[_attribute])
							{
								string attribValue = Encoding.ASCII.GetString(ba).ToLowerInvariant();
								updateDataList(objectDN, whenChanged, attribValue);
							}
						}
						else
						{
							Console.WriteLine();
							Console.Write("Could not read:  {0}", sre.DistinguishedName);
						}
					}
					Console.WriteLine();
					if (_diagnostics)
					{
						Console.WriteLine("Number Of Waits on Server:  {0}"
							, wsrc.NumberOfWaitsOnResponseFromServer.ToString("N0"));
						Console.WriteLine("Seconds Waiting on Server:  {0}.{1}"
							, wsrc.TimeSpentWaitingOnServerResponse.TotalSeconds.ToString("N0")
							, wsrc.TimeSpentWaitingOnServerResponse.Milliseconds.ToString("N0"));
					}

					return true;
				}
			}
			return false;
		}

		private static void updateDataList(string objectDN, DateTime whenChanged, string data)
		{
			System.Diagnostics.Debug.WriteLine(data + " - " + objectDN);
			//Console.WriteLine(data + " - " + objectDN + " - " + whenChanged.ToString());
			Dictionary<string, DateTime> currentValues;
			bool exists = _attributeData.TryGetValue(data, out currentValues);
			if (!exists)
			{
				Dictionary<string, DateTime> l = new Dictionary<string, DateTime>();
				l.Add(objectDN, whenChanged);
				_attributeData.Add(data, l);
			}
			else if (!currentValues.ContainsKey(objectDN))
			{
				currentValues.Add(objectDN, whenChanged);
			}
		}

		private static bool outputHelp(string incorrectSyntax)
		{
			Console.WriteLine();
			if (!string.IsNullOrEmpty(incorrectSyntax))
			{
				Console.WriteLine("Incorrect	:  {0}", incorrectSyntax);
				Console.WriteLine();
			}
			Console.WriteLine("Search the specified scope for duplicates of specified attribute.");
			Console.WriteLine("Command Line Options:  {0} [/{1}:<attributeName>] [/{2}:<distinguishedName Of NC] [/{3}:<TargetDC>] [/{5}:<CustomeLdapFilter>] [/{4}]"
				, Assembly.GetExecutingAssembly().GetName().Name
				, CMD_LINE_SWITCH_ATTRIBUTE_NAME
				, CMD_LINE_SWITCH_TARGET_NC
				, CMD_LINE_SWITCH_TARGET_DC
				, CMD_LINE_SWITCH_TARGET_DC
				, CMD_LINE_SWITCH_LDAP_FILTER);
			Console.WriteLine();
			Console.WriteLine("/{0}\tAttribute to search for."
				, CMD_LINE_SWITCH_ATTRIBUTE_NAME);
			Console.WriteLine("/{0}\tSearch only within the following NC."
				, CMD_LINE_SWITCH_TARGET_NC);
			Console.WriteLine("/{0}\tRun all queries against specific DC."
				, CMD_LINE_SWITCH_TARGET_DC);
			Console.WriteLine("/{0}\tUse a Global Catalog.  This can not be used if searching for \t\tduplicates that are not in the PAS."
				, CMD_LINE_SWITCH_USE_GC);
			Console.WriteLine("/{0}\tSupply custom LDAP filter to narrow search if desired."
				, CMD_LINE_SWITCH_LDAP_FILTER);
			Console.WriteLine();
			Console.WriteLine("If no options are specified this searchs the entire forest for duplicate SPNs.");
			Console.WriteLine("Example syntax:");
			Console.WriteLine("{0} /{1}:\"DC=somedomain,DC=someroot,DC=com\""
				, Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_TARGET_NC);
			Console.WriteLine(" - Search just within this NC for attributes with duplicate data.");
			Console.WriteLine("{0} /{1}:\"proxyAddresses\""
				, Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_ATTRIBUTE_NAME);
			Console.WriteLine(" - Search all the objects in the current forest to find duplicate proxyAddresses.");
			Console.WriteLine("{0} /{1}:\"sAMAccountName\" /{2}:\"(objectCategory=computer)\" /{3}"
				, Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_ATTRIBUTE_NAME, CMD_LINE_SWITCH_LDAP_FILTER, CMD_LINE_SWITCH_USE_GC);
			Console.WriteLine(" - Search all the computer objects in the forest to find the ones with duplicate sAMAccountName attributes");
			return false;
		}

		private static bool parseCommandLineArgs(string[] args)
		{
			foreach (string arg in args)
			{
				if (arg.Substring(0, 1).CompareTo("/") == 0)
				{
					string param = arg.Substring(1);
					string value = null;
					if (param.Contains(":"))
					{
						int indexOf = arg.IndexOf(':');
						param = arg.Substring(1, indexOf - 1).ToLowerInvariant();
						value = arg.Substring(indexOf + 1).Trim(new char[] { '"' }).ToLowerInvariant();
					}

					if (string.Compare(param, CMD_LINE_SWITCH_ATTRIBUTE_NAME, StringComparison.OrdinalIgnoreCase) == 0)
					{
						_attribute = value;
					}
					else if (string.Compare(param, CMD_LINE_SWITCH_TARGET_NC, StringComparison.OrdinalIgnoreCase) == 0)
					{
						_targetNC = value;
					}
					else if (string.Compare(param, CMD_LINE_SWITCH_TARGET_DC, StringComparison.OrdinalIgnoreCase) == 0)
					{
						_targetDC = value;
					}
					else if (string.Compare(param, CMD_LINE_SWITCH_LDAP_FILTER, StringComparison.OrdinalIgnoreCase) == 0)
					{
						_ldapFilter = value;
					}
					else if (string.Compare(param, CMD_LINE_SWITCH_USE_GC, StringComparison.OrdinalIgnoreCase) == 0)
					{
						_bindTo = LDAP_PORTS.Global_Catalog;
					}
					else if (string.Compare(param, CMD_LINE_SWITCH_DIAG, StringComparison.OrdinalIgnoreCase) == 0)
					{
						_diagnostics = true;
					}
					else
					{
						return outputHelp(arg);
					}
				}
				else
				{
					outputHelp(arg);
					return false;
				}
			}

			//Since we made it all the way through output all the commandline switches used
			foreach (string arg in args)
			{
				Console.WriteLine("Command Line Switch:  {0}", arg);
			}
			Console.WriteLine();

			return true;
		}

		private static void showActionIndicator()
		{
			switch (_trackActionIndicatorToShow)
			{
				case 0:
					Console.Write('\\');
					_trackActionIndicatorToShow = (byte)(_trackActionIndicatorToShow + 1);
					break;

				case 1:
					Console.Write('|');
					_trackActionIndicatorToShow = (byte)(_trackActionIndicatorToShow + 1);
					break;

				case 2:
					Console.Write('/');
					_trackActionIndicatorToShow = (byte)(_trackActionIndicatorToShow + 1);
					break;

				case 3:
					Console.Write('-');
					_trackActionIndicatorToShow = (byte)(_trackActionIndicatorToShow + 1);
					break;

				case 4:
					Console.Write('\\');
					_trackActionIndicatorToShow = (byte)(_trackActionIndicatorToShow + 1);
					break;

				case 5:
					Console.Write('|');
					_trackActionIndicatorToShow = (byte)(_trackActionIndicatorToShow + 1);
					break;

				case 6:
					Console.Write('-');
					_trackActionIndicatorToShow = 0;
					break;
			}
			Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
		}

		private static void outputHeader()
		{
			Console.WriteLine("Query for all duplicate attribute values in the forest.  From http://www.codeplex.com/ActiveDirectoryUtils.");
			Console.WriteLine("Version:  {0}", Assembly.GetExecutingAssembly().GetName().Version);
		}

		private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
		{
			Console.Clear();
			Console.WriteLine("This has experienced a critical error.  Please submit the below information to:\n http://www.codeplex.com/ActiveDirectoryUtils.");
			Console.WriteLine();
			outputHeader();
			Console.WriteLine(".NET Version: {0}", Environment.Version.ToString());
			Exception err = (Exception)e.ExceptionObject;
			Console.WriteLine("Type of Exception:\t{0}", err.GetType());
			Console.WriteLine("Error Message:\t{0}", err.Message);
			//Console.WriteLine("Stack Trace:\n{0}", err.StackTrace);
		}
	}
}